package com.gientech.madp.sm2.utils;

import org.apache.commons.lang.RandomStringUtils;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.*;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.DSAKCalculator;
import org.bouncycastle.crypto.signers.RandomDSAKCalculator;
import org.bouncycastle.math.ec.*;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.bouncycastle.util.BigIntegers;
import org.bouncycastle.util.Strings;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

/**
 * @Author smile_ZhongXuan
 * @Date 2021/10/8 10:12
 * @Version 1.0
 */
public class SM2Util {
	private static final String SM2_STD = "sm2p256v1";
	private static final String SM2_PID = "1234567812345678";

	/**
	 * SM2国密算法加密
	 * @param publicKey  公钥
	 * @param data  需要加密的数据
	 * @return
	 * @throws InvalidCipherTextException
	 */
	public static String encrypt(String publicKey, String data) throws InvalidCipherTextException {
		X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");

		ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters
				.getG(), sm2ECParameters.getN());

		ECPoint pukPoint = sm2ECParameters.getCurve()
				.decodePoint(Hex.decode(publicKey));

		ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);

		SM2Engine sm2Engine = new SM2Engine();
		sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));
		byte[] arrayOfBytes = null;
		byte[] cipherDataByte = Base64.encode(data.getBytes(StandardCharsets.UTF_8));
		arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
		return Hex.toHexString(arrayOfBytes);
	}

	/**
	 * SM2国密算法解密
	 * @param privateKey  私钥
	 * @param data  需要解密的数据(加密后的字符串)
	 * @return
	 * @throws InvalidCipherTextException
	 */
	public static String decrypt(String privateKey, String data) throws InvalidCipherTextException {
		X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");

		ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters
				.getG(), sm2ECParameters.getN());
		BigInteger privateKeyD = new BigInteger(privateKey, 16);
		ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);

		SM2Engine sm2Engine = new SM2Engine();
		sm2Engine.init(false, privateKeyParameters);
		byte[] arrayOfBytes = null;
		byte[] cipherDataByte = Hex.decode(data);
		arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
		return new String(Base64.decode(arrayOfBytes), StandardCharsets.UTF_8);
	}

	/**
	 * 加签
	 * @param content  需要进行加签的内容信息
	 * @param privateKey  私钥
	 * @return
	 * @throws NoSuchAlgorithmException
	 */
	public static String sign(String content, String privateKey) throws NoSuchAlgorithmException {
		byte[] message = content.getBytes();

		X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");

		ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters
				.getG(), sm2ECParameters.getN());
		BigInteger privateKeyD = new BigInteger(privateKey, 16);
		ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);

		SM2Util sm2Util = new SM2Util();
		SM2Signer sm2Signer = sm2Util.new SM2Signer();

		sm2Signer.init(true, new ParametersWithID(new ParametersWithRandom(privateKeyParameters,
				SecureRandom.getInstance("SHA1PRNG")),
				Strings.toByteArray("1234567812345678")));

		BigInteger[] bigIntegers = sm2Signer.generateSignature(message);
		byte[] rBytes = modifyRSFixedBytes(bigIntegers[0].toByteArray());
		byte[] sBytes = modifyRSFixedBytes(bigIntegers[1].toByteArray());
		byte[] signBytes = ByteUtils.concatenate(rBytes, sBytes);
		return Hex.toHexString(signBytes);
	}

	/**
	 * 验签
	 * @param content  加签前的内容信息
	 * @param publicKey  公钥
	 * @param sign  加签后的字符串
	 * @return
	 */
	public static boolean verify(String content, String publicKey, String sign) {
		byte[] message = content.getBytes();
		byte[] signData = Hex.decode(sign);

		X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");

		ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters
				.getG(), sm2ECParameters.getN());

		ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKey));

		ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);

		BigInteger R = null;
		BigInteger S = null;
		byte[] rBy = new byte[33];
		System.arraycopy(signData, 0, rBy, 1, 32);
		rBy[0] = 0;
		byte[] sBy = new byte[33];
		System.arraycopy(signData, 32, sBy, 1, 32);
		sBy[0] = 0;
		R = new BigInteger(rBy);
		S = new BigInteger(sBy);

		SM2Util sm2Util = new SM2Util();
		SM2Signer sm2Signer = sm2Util.new SM2Signer();

		ParametersWithID parametersWithID = new ParametersWithID(publicKeyParameters,
				Strings.toByteArray("1234567812345678"));

		sm2Signer.init(false, parametersWithID);

		boolean verify = sm2Signer.verifySignature(message, R, S);
		return verify;
	}

	/**
	 * 校验密钥
	 * @param publicKey  公钥
	 * @param privateKey  私钥
	 * @return
	 * @throws NoSuchAlgorithmException
	 */
	public static boolean match(String publicKey, String privateKey) throws NoSuchAlgorithmException {
		String content = RandomStringUtils.random(50, true, true);
		String sign = sign(content, privateKey);
		boolean verify = verify(content, publicKey, sign);
		return verify;
	}

	private static byte[] modifyRSFixedBytes(byte[] rs) {
		int length = rs.length;
		int fixedLength = 32;
		byte[] result = new byte[fixedLength];
		if (length < 32)
			System.arraycopy(rs, 0, result, fixedLength - length, length);
		else {
			System.arraycopy(rs, length - fixedLength, result, 0, fixedLength);
		}
		return result;
	}

	public static AsymmetricCipherKeyPair generateKeyPair() throws NoSuchAlgorithmException {
		X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");

		ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters
				.getG(), sm2ECParameters.getN());

		ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator();

		keyPairGenerator.init(new ECKeyGenerationParameters(domainParameters,
				SecureRandom.getInstance("SHA1PRNG")));

		AsymmetricCipherKeyPair asymmetricCipherKeyPair = keyPairGenerator.generateKeyPair();
		return asymmetricCipherKeyPair;
	}



	public class SM2Signer implements DSA, ECConstants {
		private final DSAKCalculator kCalculator = new RandomDSAKCalculator();
		private byte[] userID;
		private int curveLength;
		private ECDomainParameters ecParams;
		private ECPoint pubPoint;
		private ECKeyParameters ecKey;

		public void init(boolean paramBoolean, CipherParameters paramCipherParameters) {
			CipherParameters localCipherParameters;
			if ((paramCipherParameters instanceof ParametersWithID)) {
				localCipherParameters = ((ParametersWithID) paramCipherParameters).getParameters();
				this.userID = ((ParametersWithID) paramCipherParameters).getID();
			} else {
				localCipherParameters = paramCipherParameters;
				this.userID = new byte[0];
			}
			if (paramBoolean) {
				if ((localCipherParameters instanceof ParametersWithRandom)) {
					ParametersWithRandom localParametersWithRandom = (ParametersWithRandom) localCipherParameters;
					this.ecKey = ((ECKeyParameters) localParametersWithRandom.getParameters());
					this.ecParams = this.ecKey.getParameters();
					this.kCalculator.init(this.ecParams.getN(), localParametersWithRandom.getRandom());
				} else {
					this.ecKey = ((ECKeyParameters) localCipherParameters);
					this.ecParams = this.ecKey.getParameters();
					this.kCalculator.init(this.ecParams.getN(), new SecureRandom());
				}
				this.pubPoint = this.ecParams.getG().multiply(((ECPrivateKeyParameters) this.ecKey).getD()).normalize();
			} else {
				this.ecKey = ((ECKeyParameters) localCipherParameters);
				this.ecParams = this.ecKey.getParameters();
				this.pubPoint = ((ECPublicKeyParameters) this.ecKey).getQ();
			}
			this.curveLength = ((this.ecParams.getCurve().getFieldSize() + 7) / 8);
		}

		public BigInteger[] generateSignature(byte[] paramArrayOfByte) {
			SM3Digest localSM3Digest = new SM3Digest();
			byte[] arrayOfByte1 = getZ(localSM3Digest);
			localSM3Digest.update(arrayOfByte1, 0, arrayOfByte1.length);
			localSM3Digest.update(paramArrayOfByte, 0, paramArrayOfByte.length);
			byte[] arrayOfByte2 = new byte[localSM3Digest.getDigestSize()];
			localSM3Digest.doFinal(arrayOfByte2, 0);
			BigInteger localBigInteger1 = this.ecParams.getN();
			BigInteger localBigInteger2 = calculateE(arrayOfByte2);
			BigInteger localBigInteger3 = ((ECPrivateKeyParameters) this.ecKey).getD();
			ECMultiplier localECMultiplier = createBasePointMultiplier();
			BigInteger localBigInteger4;
			BigInteger localBigInteger5;
			do {
				BigInteger localBigInteger6;
				do {
					localBigInteger6 = this.kCalculator.nextK();
					ECPoint localObject = localECMultiplier.multiply(this.ecParams.getG(), localBigInteger6).normalize();
					localBigInteger4 = localBigInteger2.add(localObject.getAffineXCoord().toBigInteger()).mod(localBigInteger1);
				}
				while ((localBigInteger4.equals(ZERO)) || (localBigInteger4.add(localBigInteger6).equals(localBigInteger1)));
				Object localObject = localBigInteger3.add(ONE).modInverse(localBigInteger1);
				localBigInteger5 = localBigInteger6.subtract(localBigInteger4.multiply(localBigInteger3)).mod(localBigInteger1);
				localBigInteger5 = ((BigInteger) localObject).multiply(localBigInteger5).mod(localBigInteger1);
			}
			while (localBigInteger5.equals(ZERO));
			return new BigInteger[]{localBigInteger4, localBigInteger5};
		}

		public boolean verifySignature(byte[] paramArrayOfByte, BigInteger paramBigInteger1, BigInteger paramBigInteger2) {
			BigInteger localBigInteger1 = this.ecParams.getN();
			if ((paramBigInteger1.compareTo(ONE) < 0) || (paramBigInteger1.compareTo(localBigInteger1) > 0))
				return false;
			if ((paramBigInteger2.compareTo(ONE) < 0) || (paramBigInteger2.compareTo(localBigInteger1) > 0))
				return false;
			ECPoint localECPoint1 = ((ECPublicKeyParameters) this.ecKey).getQ();
			SM3Digest localSM3Digest = new SM3Digest();
			byte[] arrayOfByte1 = getZ(localSM3Digest);
			localSM3Digest.update(arrayOfByte1, 0, arrayOfByte1.length);
			localSM3Digest.update(paramArrayOfByte, 0, paramArrayOfByte.length);
			byte[] arrayOfByte2 = new byte[localSM3Digest.getDigestSize()];
			localSM3Digest.doFinal(arrayOfByte2, 0);
			BigInteger localBigInteger2 = calculateE(arrayOfByte2);
			BigInteger localBigInteger3 = paramBigInteger1.add(paramBigInteger2).mod(localBigInteger1);
			if (localBigInteger3.equals(ZERO))
				return false;
			ECPoint localECPoint2 = this.ecParams.getG().multiply(paramBigInteger2);
			localECPoint2 = localECPoint2.add(localECPoint1.multiply(localBigInteger3)).normalize();
			return paramBigInteger1.equals(localBigInteger2.add(localECPoint2.getAffineXCoord().toBigInteger()).mod(localBigInteger1));
		}

		private byte[] getZ(Digest paramDigest) {
			addUserID(paramDigest, this.userID);
			addFieldElement(paramDigest, this.ecParams.getCurve().getA());
			addFieldElement(paramDigest, this.ecParams.getCurve().getB());
			addFieldElement(paramDigest, this.ecParams.getG().getAffineXCoord());
			addFieldElement(paramDigest, this.ecParams.getG().getAffineYCoord());
			addFieldElement(paramDigest, this.pubPoint.getAffineXCoord());
			addFieldElement(paramDigest, this.pubPoint.getAffineYCoord());
			byte[] arrayOfByte = new byte[paramDigest.getDigestSize()];
			paramDigest.doFinal(arrayOfByte, 0);
			return arrayOfByte;
		}

		private void addUserID(Digest paramDigest, byte[] paramArrayOfByte) {
			int i = paramArrayOfByte.length * 8;
			paramDigest.update((byte) (i >> 8 & 0xFF));
			paramDigest.update((byte) (i & 0xFF));
			paramDigest.update(paramArrayOfByte, 0, paramArrayOfByte.length);
		}

		private void addFieldElement(Digest paramDigest, ECFieldElement paramECFieldElement) {
			byte[] arrayOfByte = BigIntegers.asUnsignedByteArray(this.curveLength, paramECFieldElement.toBigInteger());
			paramDigest.update(arrayOfByte, 0, arrayOfByte.length);
		}

		protected ECMultiplier createBasePointMultiplier() {
			return new FixedPointCombMultiplier();
		}

		protected BigInteger calculateE(byte[] paramArrayOfByte) {
			return new BigInteger(1, paramArrayOfByte);
		}
	}
}
